W5. Шаблоны C++, метапрограммирование RANGE, умные указатели

Автор

Zakhar Podyakov

Дата публикации

18 февраля 2026 г.

1. Краткое содержание

1.1 Зачем нужны шаблоны? Проблема дублирования кода

Представьте, что нужна функция, возвращающая максимум из двух целых. Это просто:

int Max(int a, int b) {
    return a > b ? a : b;
}

Теперь та же логика нужна для float, double и пользовательского типа Temperature. Наивный путь — написать отдельную функцию под каждый тип:

float  Max(float a,  float b)  { return a > b ? a : b; }
double Max(double a, double b) { return a > b ? a : b; }
// ... и так далее для каждого типа

У такого подхода есть серьёзные недостатки:

  • Сложно сопровождать: любое исправление ошибки приходится переносить во все копии; тесты должны покрывать все варианты.
  • Нереализуемо для библиотек: автор библиотеки заранее не знает все типы, которые понадобятся пользователю.
  • Плохо масштабируется: в больших программах (тысячи типов) это становится неуправляемым.

Выход — genericity (обобщённость, параметрический полиморфизм): записать алгоритм один раз и позволить компилятору автоматически порождать версии под конкретные типы. В C++ это делает механизм templates (шаблонов). В других языках похожие идеи называются иначе:

  • Ada, Eiffel: Generics
  • Java, C#, Rust, Swift, Scala: Generics
  • C++: Templates

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Один обобщённый шаблон заменяет множество почти одинаковых реализаций под конкретные типы"
%%| fig-width: 6
%%| fig-height: 3
flowchart TB
    Dup["Max(int,int)<br/>Max(float,float)<br/>Max(double,double)<br/>Max(Temperature,Temperature)"]
    Tpl["template<typename T><br/>T Max(T a, T b)"]
    Dup --> Tpl

1.2 Шаблоны функций (function templates)
1.2.1 Синтаксис

Function template (шаблон функции) — это «чертёж» для семейства функций. Вместо конкретного типа вроде int используется type parameter (параметр типа) — имя-заполнитель, обычно T:

template <typename T>   // Template header: declares T as a type parameter
T Max(T a, T b)         // Template body: uses T wherever the type appears
{
    return a > b ? a : b;
}
// No semicolon after the closing brace
  • template — ключевое слово, с которого начинается объявление шаблона.
  • typename T — объявляет T формальным параметром типа (можно писать и class T — здесь это эквивалентно).
  • Тело шаблона такое же, как для конкретного типа, только вместо него везде стоит T.
1.2.2 Инстанцирование шаблона (template instantiation): что происходит «под капотом»

Когда вы вызываете шаблон функции, компилятор выполняет template instantiation (инстанцирование шаблона): смотрит на типы фактических аргументов, порождает конкретную функцию (её называют function-by-template — функцией, полученной из шаблона), и компилирует её.

double x = 3.5, y = 2.1, res;
res = Max(x, y);   // Compiler sees: both args are double
                   // → generates and compiles: double Max_double(double a, double b) { ... }
                   // → replaces the call with: res = Max_double(x, y);

Весь процесс automatic (автоматический) — вы не пишете Max_double сами: это делает компилятор. Имя Max_double здесь условное; реальный компилятор использует name mangling (искажение имён).

Ключевые правила инстанцирования:

  • Фактический тип выводится по типам аргументов, а не по типу возвращаемого значения.
  • Каждая уникальная комбинация типов инстанцируется один раз, даже если вызывать функцию много раз с теми же типами.
  • Каждая другая комбинация типов даёт отдельную function-by-template:
res = Max(x, y);          // Generates Max_double
int k = Max(1, (int)res); // Generates Max_int (a different function!)
1.2.3 Алгоритм инстанцирования (по шагам)

Когда компилятор встречает вызов f(actual-arguments):

  1. Если fобычная функция (regular function) → вызов компилируется напрямую.
  2. Если ffunction template:
    1. Определить тип \(T_i\) каждого фактического аргумента.
    2. Если function-by-template для этого набора \(\{T_i\}\) уже существует → перейти к шагу 3.
    3. Сгенерировать function-by-template, подставив \(T_i\) вместо формальных параметров типа.
    4. Скомпилировать сгенерированную функцию.
  3. Сгенерировать код вызова функции.
1.2.4 Раздувание кода (code bloat)

Инстанцирование шаблонов может привести к code bloat (раздуванию кода): если два отдельно компилируемых файла включают один и тот же заголовок с шаблоном и используют одни типы, линкер может получить две одинаковые копии одной и той же function-by-template в исполняемом файле:

T.h          →  File1.cpp (includes T.h) → File1.obj  (contains Max_double)
             →  File2.cpp (includes T.h) → File2.obj  (contains Max_double)
                                         → App.exe    (TWO copies of Max_double!)

Современные линкеры умеют сливать дубликаты, а стандарт C++ даёт средства (explicit instantiation, extern template) для контроля — но в крупных проектах это реальная забота.

1.2.5 Требования к фактическим типам

Шаблон не работает с любым типом — только с теми, для которых определены все операции, используемые в теле. Для Max в теле есть a > b, значит фактический тип должен иметь operator>.

class C {
    int m;
public:
    C() : m(0) { }
    // No operator> defined!
};

C c1, c2;
Max(c1, c2);  // Compile error: 'binary >': 'class C' doesn't define this operator

Сообщение об ошибке обычно указывает на сгенерированную функцию вроде Max_C.

Решение: добавить в класс нужный оператор:

class C {
    int m;
public:
    C() : m(0) { }
    bool operator>(const C& c) const { return m > c.m; }
};
// Now Max(c1, c2) works correctly

Общий принцип: function template неявно требует, чтобы каждый фактический тип поддерживал все операции из тела шаблона. Нарушение — ошибка времени компиляции (compile-time error).

1.2.6 C++20 Concepts: формальные требования к типу

До C++20 эти требования оформлялись неформально (комментариями) и проявлялись только при инстанцировании — часто с запутанными диагностиками. Concepts (концепты) позволяют формально зафиксировать требования:

template<typename T>
concept GreaterThan =
    requires(T x, T y) { { x > y } -> std::same_as<bool>; };

template<typename T> requires GreaterThan<T>
T Max(T a, T b) {
    return a > b ? a : b;
}

Теперь при вызове Max с типом без operator> компилятор сообщает о нарушенном concept, а не о «внутренней» ошибке глубоко в теле шаблона.

1.2.7 Явное инстанцирование шаблонов функций

Иногда компилятор не может вывести параметр шаблона из аргументов — например, у функции нет параметров:

template <typename T>
int spaceOf() {
    int bytes = sizeof(T);
    return bytes / 4 + (bytes % 4 > 0 ? 1 : 0);
}

// int w = spaceOf();  // ERROR: compiler cannot deduce T

Выход — explicit instantiation (явное инстанцирование): указать тип в угловых скобках в месте вызова:

int wint = spaceOf<int>();      // Explicitly: T = int
int wdouble = spaceOf<double>(); // Explicitly: T = double

Компилятор инстанцирует шаблон с заданным типом и может применить оптимизации (например, sizeof(int)compile-time constant, и всю функцию можно свернуть в константу):

spaceOf<int>()  →  generates spaceOf_int()  →  inlines to constant 1
int wint = spaceOf<int>();  →  int wint = 1;
1.3 Шаблоны классов (class templates)

Class template (шаблон класса) — чертёж для семейства классов, так же как function template задаёт семейство функций.

  • Класс — это тип.
  • Шаблон классане тип; это семейство типов.
1.3.1 Мотивирующий пример: стек

Stack (стек), ещё говорят LIFO (Last In, First Out — «последним пришёл, первым ушёл») — структура данных с тремя базовыми операциями:

  • push: положить элемент наверх.
  • pop: снять и вернуть верхний элемент.
  • isEmpty: проверить, пуст ли стек.

Реализация на C++ без шаблонов для целых выглядит так:

class Stack {
    int top;
    int S[100];          // Array of integers
public:
    Stack() : top(-1) { }
    void push(int V) { S[++top] = V; }
    int  pop()       { return S[top--]; }
    bool isEmpty()   { return top < 0; }
};

Чтобы получить стек double, пришлось бы копировать весь класс и заменить каждый int на double — та же проблема дублирования, что и у шаблонов функций. Решение через шаблон:

template <typename T>       // T is the element type
class Stack {
    int top;
    T S[100];               // Array of T
public:
    Stack() : top(-1) { }
    void push(T V) { S[++top] = V; }
    T    pop()     { return S[top--]; }
    bool isEmpty() { return top < 0; }
};

Теперь T может быть любым типом — целым, double, строкой или пользовательским.

1.3.2 Инстанцирование шаблона класса

В отличие от шаблонов функций (их инстанцируют неявно по типам аргументов вызова), class template нужно инстанцировать явно, синтаксисом <actual-type>:

Stack<int>    sint;         // Stack of integers
Stack<double> sdouble;      // Stack of doubles
Stack<string> sstr;         // Stack of strings

Stack<float>  sf1, sf2;     // Two stacks of floats
Stack<int>*   arrayOfStacks[10]; // Array of 10 pointers to int-stacks

Запись Stack<int> — это type specifier (спецификатор типа): имя класса, который компилятор порождает из шаблона Stack, подставив int вместо T. Такой класс ведёт себя как обычный.

Использование class-by-template:

Stack<int> s;
s.push(1);
s.push(2);
int v = s.pop();  // v = 2

Type aliases (псевдонимы типов) — два эквивалентных синтаксиса:

typedef Stack<double> SD;        // C++98/03
using SD = Stack<double>;        // C++11 (preferred)
SD sd1, sd2;
1.3.3 Шаблон, класс и экземпляр — три уровня

Важно различать три сущности:

Уровень Сущность Кто создаёт Когда существует
Шаблон Stack<T> (чертёж) программист исходный код
Class-by-template Stack<int>, Stack<double> компилятор (instantiation) compile time
Объект (instance) Stack<int> s; runtime (new или стек) runtime

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "От определения шаблона к инстанцированному классу и объекту в runtime"
%%| fig-width: 6.3
%%| fig-height: 3
flowchart LR
    Def["template<typename T><br/>class Stack"]
    Spec1["Stack<int>"]
    Spec2["Stack<double>"]
    Obj["Stack<int> s"]
    Def --> Spec1
    Def --> Spec2
    Spec1 --> Obj

1.3.4 Требования к фактическим типам в шаблонах классов

Внутри Stack метод push делает:

void push(T V) { S[++top] = V; }

Используются две операции над T:

  1. Передача V по значению (T V) — вызывается copy constructor (конструктор копирования).
  2. Присваивание = V — вызывается assignment operator (оператор присваивания).

Поэтому для Stack<MyClass> у MyClass должны быть публичный конструктор копирования и публичный оператор присваивания. Компилятор обычно генерирует их сам, но бывают исключения (дорогое копирование, = delete). Чтобы не вызывать копирование при входе в push, передавайте по ссылке:

void push(const T& V) { S[++top] = V; }   // No copy constructor needed for the call

Аналогично, pop по значению тоже копирует. Часто улучшают возвратом по ссылке:

T& pop() { return S[top--]; }
1.3.5 Нетиповые параметры шаблона (non-type template parameters)

Параметры шаблона — не обязаны быть типами: это могут быть constant values (константные значения) — целые, указатели и т.д. Тогда поведение или «размеры» класса задаются на compile time, а не только тип элемента.

Фиксированный массив T S[100] в Stack — ограничение дизайна: у каждого стека ровно 100 ячеек. Размер можно сделать параметром шаблона:

template <typename T, int N>    // Two parameters: type T and integer N
class Stack {
    int top;
    T S[N];                      // Size is now N, determined at compile time
public:
    Stack() : top(-1) { }
    void push(const T& V) { S[++top] = V; }
    T    pop()            { top--; return S[top + 1]; }
    bool isEmpty()        { return top < 0; }
};

Использование:

Stack<int, 10>  s10;   // Stack of 10 integers
Stack<double, 50> s50; // Stack of 50 doubles

Важное ограничение: аргументы non-type шаблона должны быть compile-time constants. Переменную runtime в такой параметр подставить нельзя.

Допустимые non-type аргументы:

  • константные целочисленные выражения (например 10, sizeof(int) * 4);
  • имена объектов с external linkage;
  • адреса объектов/функций с external linkage.
1.3.6 Шаблоны классов с несколькими параметрами типа

У шаблона может быть несколько параметров типа. Для Dictionary (отображение ключ → значение) естественно два типа:

#include <map>
#include <string>

template<typename K, typename V>
class Dictionary {
    std::map<K, V> data;
public:
    void insert(const K& key, const V& value) {
        data[key] = value;
    }
    V get(const K& key) const {
        auto it = data.find(key);
        if (it != data.end()) return it->second;
        throw std::runtime_error("Key not found");
    }
    bool contains(const K& key) const {
        return data.find(key) != data.end();
    }
    void remove(const K& key) {
        data.erase(key);
    }
};

// Usage:
Dictionary<int, std::string> myDict;
myDict.insert(1, "One");
myDict.insert(2, "Two");
std::cout << myDict.get(1) << std::endl;  // "One"
1.4 Метапрограммирование на примере типов RANGE

Metaprogramming (метапрограммирование) — использование системы типов и шаблонов, чтобы проверять ограничения на этапе компиляции, а не в runtime. Пример RANGE это хорошо иллюстрирует.

1.4.1 Проблема «голого» целого

Пусть currentDay должен хранить день месяца (1–31). Если взять int:

int currentDay, currentMonth;
currentDay = 70;              // абсурд, но компилятор это пропустит
currentDay = currentMonth + 1; // смешать день и месяц? предупреждения не будет

В Pascal и Ada это решают range types (типами-диапазонами):

type DayOfMonth = 1..31;  // Pascal
type DayOfMonth is Integer range 1..31;  -- Ada

В C++ встроенных range types нет, но на классах и шаблонах такой тип можно построить.

1.4.2 Первая попытка: наивный класс RANGE

Идея: класс хранит значение вместе с допустимыми границами и проверяет значение при каждом изменении.

class RANGE {
    int leftBorder;
    int rightBorder;
    int value;
public:
    // Constructor: set borders and initial value
    RANGE(int v, int l, int r) {
        leftBorder = l;
        rightBorder = r;
        value = v;
        check();  // Verify immediately
    }

    // Deleted default constructor: forbid uninitialized RANGE objects
    RANGE() = delete;

    // Copy constructor
    RANGE(const RANGE& r) {
        leftBorder = r.leftBorder;
        rightBorder = r.rightBorder;
        value = r.value;
    }

    // Assignment from another RANGE
    RANGE& operator=(RANGE& r) { value = r.value; return *this; }

    // Assignment from int
    RANGE& operator=(int v) { value = v; check(); return *this; }

    // Increment
    RANGE& operator++() { value++; check(); return *this; }

    // Conversion to int (so RANGE can be used where int is expected)
    operator int() { return value; }

private:
    void check() {
        if (value < leftBorder || value > rightBorder)
            throw std::out_of_range("RANGE value out of bounds");
    }
};

Почему operator= возвращает *this? Чтобы работали цепочки присваиваний: в a = b = c присваивание должно возвращать ссылку на левый операнд.

Использование:

RANGE range(0, -10, 10);  // value=0, allowed: [-10, 10]
range = 5;    // OK
range = 15;   // throws exception
++range;      // increments, checks
int i = range; // uses operator int()
1.4.3 Недостатки наивного подхода

Код работает, но есть два фундаментальных слабых места:

Проблема 1 — границы в значении, а не в типе:

RANGE a(0, -5, 5);
RANGE b(3, 1, 10);

a = b;  // Compiles! But semantically wrong — different ranges!

a и b имеют один и тот же тип (RANGE), хотя описывают разные области значений. Хотелось бы, чтобы RANGE(-5,5) и RANGE(1,10) были разными типами, и присваивание одного другому давало ошибку компиляции (compile-time error).

Проблема 2 — лишняя память:

В каждом объекте RANGE три целых: value, leftBorder, rightBorder. Границы после конструктора не меняются — логически это часть типа, а не значения. Нет смысла хранить их в каждом экземпляре.

1.4.4 Шаблонное решение для RANGE

Границы делаем template parameters (параметрами шаблона) — compile-time constants, а не полями runtime. Тогда каждая пара (leftBorder, rightBorder) порождает отдельный тип:

template <int leftBorder, int rightBorder>
class RANGE {
    int value;            // Only member: the stored value
    RANGE() = delete;     // No default constructor

public:
    RANGE(int v)             { value = v; check(); }
    RANGE(const RANGE& r)    { value = r.value; }
    RANGE& operator=(RANGE& r)  { value = r.value; return *this; }
    RANGE& operator=(int v)     { value = v; check(); return *this; }
    RANGE& operator++()         { value++; check(); return *this; }
    operator long()             { return (long)value; }

private:
    void check() {
        if (value < leftBorder || value > rightBorder)
            throw std::out_of_range("RANGE value out of bounds");
    }
};

Теперь:

RANGE<-5, 5>  a(0);   // Type: RANGE<-5,5>, stores only one int
RANGE<1, 10>  b(3);   // Type: RANGE<1,10>, completely different type

a = b;  // COMPILE ERROR: different types — assignment operator is type-safe!

RANGE<-5,5> и RANGE<1,10>разные классы, сгенерированные из одного шаблона. В объекте хранится одно целое; границы «запечены» в имени типа на этапе компиляции — нулевая стоимость в runtime (zero runtime overhead).

Type aliases упрощают запись:

typedef RANGE<-5, 5>  myTinyInt;   // C++98
using myTinyInt = RANGE<-5, 5>;    // C++11 (preferred)

myTinyInt i = 2;   // Clean, type-safe syntax

Семейства RANGE<-5,5>, RANGE<1,10>, RANGE<100,1000> и т.д. — взаимно несовместимые типы из одного шаблона, подобно тому как несовместимы int и double.

1.5 Проблемы «сырых» указателей C/C++ (raw pointers)

Прежде чем исправлять указатели, важно понять, в чём беда. Скотт Мейерс выделил шесть категорий проблем raw pointers (плюс другие).

1.5.1 Неоднозначность: один объект или массив (проблемы 1 и 4)

Указатель T* ptr может указывать и на один объект, и на первый элемент массива — по одному типу указателя это не различить:

int x;
int A1[10];
int* A2 = &x;
int* A = cond ? A1 : A2;

int res = A[5];  // Is this valid? Depends on cond — undefined behavior!

Отсюда два разных оператора освобождения — delete ptr (один объект) и delete[] ptr (массив); перепутать их — undefined behavior (неопределённое поведение).

1.5.2 Неясность владения (проблема 2)

По объявлению T* ptr в сигнатуре функции не видно, владеет ли функция объектом (должна ли его уничтожать):

void fun(T* ptr) {
    // Do some work with *ptr
    // Should we destroy it? We don't know!
    return;
}

Такая неоднозначность ведёт к memory leaks (никто не вызвал delete) или к double-deletion (несколько путей пытаются удалить одно и то же).

1.5.3 Неясность способа уничтожения (проблема 3)

Даже если «надо удалить», неочевидно как:

void fun(T* ptr) {
    // Work...
    free(ptr);         // Correct? Or should it be:
    // myDealloc(ptr); // a custom deallocator?
    // delete ptr;     // Or this?
}

Разные схемы выделения требуют разных функций освобождения.

1.5.4 Двойное уничтожение (проблема 5)

Когда указатель разделяют несколько путей кода, легко уничтожить объект дважды:

void lib_fun(T* ptr) {
    // Does lib_fun delete the object? Hard to know without reading all source code.
}

void user_fun() {
    T* ptr = new T();
    lib_fun(ptr);
    delete ptr;  // Is this a double-delete if lib_fun already deleted it?
}

Double-deletionundefined behavior: может повредить heap и открыть уязвимости.

1.5.5 Висячие указатели (dangling pointers), проблема 6

Встроенного способа узнать, жив ли ещё объект по указателю, нет:

T* ptr = new T();
if (condition) delete ptr;
// ...
// Long code later...
// Is ptr still valid? Cannot know without tracking control flow manually.

Dangling pointer (висячий указатель) — ссылается на память, уже освобождённую. Разыменование — undefined behavior.

Классический висячий указатель со стеком:

int* p;

void f() {
    int A[10];
    p = A + 2;   // p points into f's stack frame
}               // A goes out of scope — memory is freed

int main() {
    f();
    *p = 777;   // p is now dangling — undefined behavior!
}

После возврата из f кадр стека (вместе с A) недействителен, а p всё ещё хранит адрес в этой памяти.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Висячий указатель: указатель переживает уничтоженный стековый объект"
%%| fig-width: 6.2
%%| fig-height: 3.2
flowchart TB
    subgraph Frame["Кадр стека функции f"]
      A["A : int = 42"]
      P["p = &A"]
    end
    After["возврат из f<br/>кадр снят"]
    Dangling["p хранит старый адрес<br/>dangling pointer"]
    Frame --> After --> Dangling
    style Frame fill:#f9fbfd,stroke:#355c7d,color:#1f2d3d
    style A fill:#d6eef5,stroke:#355c7d,color:#1f2d3d
    style P fill:#e8f4f8,stroke:#355c7d,color:#1f2d3d
    style After fill:#eef3f7,stroke:#355c7d,color:#1f2d3d
    style Dangling fill:#f9d9e2,stroke:#355c7d,color:#1f2d3d

1.5.6 Утечки памяти (проблема 7)

Если последний (или единственный) указатель на объект в heap выходит из области видимости без delete, объект остаётся в памяти, но недостижим — это memory leak (утечка памяти):

void f() {
    int* p = new int(42);  // Dynamic object allocated on heap
    // ... (no delete)
}  // p goes out of scope; the int(42) still lives on the heap but cannot be reached!

В долгоживущих процессах накопленные утечки могут исчерпать память.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Утечка: объект в куче остаётся выделенным, когда владеющий указатель исчез"
%%| fig-width: 6
%%| fig-height: 3
flowchart LR
    P["p : int*"]
    H["объект в куче<br/>new int(42)"]
    Gone["p вышел из области видимости"]
    Leak["недостижимый объект<br/>memory leak"]
    P --> H
    H --> Gone --> Leak

1.6 Умные указатели (smart pointers)

Smart pointers (умные указатели) — это class templates, оборачивающие raw pointers и добавляющие автоматическое управление ресурсом: «умное» поведение вроде автоматического delete при сохранении эффективности, близкой к сырому указателю. Нужен #include <memory>.

Ключевой приём — RAII (Resource Acquisition Is Initialization): ресурс захватывается в конструкторе и освобождается в деструкторе. Деструктор вызывается при выходе объекта из области видимости, значит очистка предсказуема.

1.6.1 Общая идея: простейший умный указатель

Минимальный вариант оборачивает raw pointer и вызывает delete при уничтожении обёртки:

template <typename T>
class smart_pointer {
    T* obj;           // The underlying raw pointer
public:
    smart_pointer(T* o) : obj(o) { }    // Takes ownership
    ~smart_pointer() { delete obj; }    // Guaranteed cleanup

    T* operator->() { return obj; }     // Arrow dereference
    T& operator*()  { return *obj; }    // Star dereference
};
{
    smart_pointer<MyClass> sp(new MyClass());
    sp->someMethod();  // Works like a raw pointer
}  // Destructor called — MyClass object is deleted automatically

Вызывающему коду delete не нужен. В стандартной библиотеке C++ — три «промышленных» шаблона умных указателей.

1.6.2 unique_ptr — исключительное владение

unique_ptr реализует exclusive ownership (исключительное владение): в каждый момент ровно один unique_ptr владеет объектом. Копирование запрещено на уровне типа:

#include <memory>

std::unique_ptr<int> x(new int(42));
std::unique_ptr<int> y;

y = x;                   // COMPILE ERROR: copy is forbidden
std::unique_ptr<int> z(x); // COMPILE ERROR: copy constructor is deleted

Передача владения — через std::move: владение переносится, источник обнуляется (nullifies):

y = std::move(x);   // y now owns the object; x becomes nullptr

Фабрика C++14make_unique: предпочтительнее голого new, потому что exception-safe (безопаснее при исключениях):

auto x = std::make_unique<int>(42);   // No explicit new

Основные методы:

  • x.get() — сырой указатель без отказа от владения;
  • x.reset() — уничтожить объект и обнулить указатель;
  • x.release() — вернуть сырой указатель и отказаться от владения (дальше ответственность на вызывающем).

unique_ptr снимает:

  • memory leaks (автоудаление);
  • double deletion (единственный владелец);
  • неясность владения (намерение видно из типа).
1.6.3 shared_ptr — совместное владение и ARC

shared_ptr даёт shared ownership (совместное владение): несколько shared_ptr могут владеть одним объектом. Удаление происходит, когда уничтожается последний shared_ptr на этот объект.

Механизм — Automatic Reference Counting (ARC) (автоматический подсчёт ссылок): ведётся reference counter; при копировании счётчик растёт, при уничтожении — падает; при нуле объект удаляется.

auto x = std::make_shared<int>(42);  // ref count = 1
{
    auto y = x;                       // ref count = 2 (y shares ownership)
    auto z = x;                       // ref count = 3
}  // y and z go out of scope — ref count drops to 1
// x goes out of scope — ref count drops to 0 — object deleted

Где хранится счётчик? В control block (блоке управления), часто рядом с объектом (особенно при make_shared). Внутри shared_ptr обычно два указателя: на объект и на control block.

Основные методы:

  • x.use_count() — текущий reference count;
  • x.get(), x.reset(), x.swap() — по смыслу как у unique_ptr;
  • operator bool() — проверка на ненулевой указатель.

Накладные расходы shared_ptr: два указателя на экземпляр, атомарные операции со счётчиком (для потокобезопасности счёта владельцев). Если разделение не нужно — берите unique_ptr.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "shared_ptr: несколько дескрипторов ведут к одному объекту через общий счётчик"
%%| fig-width: 6.2
%%| fig-height: 3
flowchart LR
    A["shared_ptr a"]
    B["shared_ptr b"]
    RC["control block<br/>ref count = 2"]
    Obj["управляемый объект"]
    A --> RC
    B --> RC
    RC --> Obj

1.6.4 Циклические ссылки: проблема shared_ptr

У ARC для shared_ptr есть критичный предел — circular references (циклические ссылки). Если два объекта держат друг друга shared_ptr, счётчики не обнулятся — объекты не удалятся:

class Bar;
class Foo {
public:
    std::shared_ptr<Bar> bar;
};
class Bar {
public:
    std::shared_ptr<Foo> foo;
};

void fun() {
    auto foo = std::make_shared<Foo>();  // foo ref count = 1
    foo->bar = std::make_shared<Bar>();  // bar ref count = 1
    foo->bar->foo = foo;                 // foo ref count = 2 (circular!)
}
// fun exits: foo variable destroyed → foo ref count = 1 (not 0!)
//            bar's shared_ptr<Foo> still holds foo
// Both objects are leaked!

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Цикл shared_ptr не даёт счётчику обнулиться"
%%| fig-width: 6.2
%%| fig-height: 3.2
flowchart LR
    Foo["объект Foo"]
    Bar["объект Bar"]
    Foo -- "shared_ptr<Bar>" --> Bar
    Bar -- "shared_ptr<Foo>" --> Foo
    Leak["оба живы<br/>цикл ссылок"]
    Foo --> Leak
    Bar --> Leak

1.6.5 weak_ptr — разрыв циклов

weak_ptrnon-owning (не владеющий) наблюдатель за объектом под shared_ptr. Он не увеличивает reference count, поэтому не удерживает объект от удаления. Зато можно проверить, жив ли объект, и временно получить shared_ptr для безопасного доступа.

auto shared = std::make_shared<MyObj>();  // ref count = 1
std::weak_ptr<MyObj> weak(shared);        // ref count still = 1 (weak does not count)

shared = nullptr;                          // ref count drops to 0 — object deleted
// weak now points to an expired (destroyed) object

if (weak.expired()) {
    // Object is gone
}

auto temp = weak.lock();  // Attempts to get a shared_ptr
if (temp) {
    // Object still exists — use temp safely
}

Разрыв цикла через weak_ptr: одну дугу цикла делают weak_ptr:

class Bar;
class Foo {
public:
    std::shared_ptr<Bar> bar;   // Foo owns Bar (strong reference)
};
class Bar {
public:
    std::weak_ptr<Foo> foo;     // Bar observes Foo but does NOT own it (weak reference)
};

void fun() {
    auto foo = std::make_shared<Foo>();  // foo ref count = 1
    foo->bar = std::make_shared<Bar>();  // bar ref count = 1
    foo->bar->foo = foo;                 // foo ref count stays 1 (weak_ptr!)
}
// fun exits: foo ref count = 1 → 0 → Foo deleted → Bar's shared_ptr lost → bar ref count = 1 → 0 → Bar deleted
// No leak!

Сводка по типам умных указателей:

Тип Владение Копия ref count Когда использовать
unique_ptr Exclusive Нет (только move) Нет Один владелец, без разделения
shared_ptr Shared Да Да (ARC) Несколько владельцев
weak_ptr Нет (наблюдатель) Да Нет Разрыв циклов, «слабые» ссылки

2. Определения

  • Template (шаблон): средство C++, позволяющее писать обобщённый код с параметрами типа или значений compile time, чтобы компилятор порождал конкретные функции или классы под заданный тип.
  • Function template (шаблон функции): шаблон как чертёж семейства функций; компилятор каждый раз порождает конкретную function-by-template при использовании с конкретным типом.
  • Class template (шаблон класса): шаблон как чертёж семейства классов; перед использованием нужно явное инстанцирование вида TemplateName<ActualType>.
  • Template parameter (параметр шаблона): заполнитель в определении шаблона — либо type parameter (typename T / class T), либо non-type parameter (константа compile time, например int N).
  • Template instantiation (инстанцирование шаблона): процесс, в котором компилятор подставляет фактические типы (или значения) вместо формальных параметров и получает конкретную функцию или класс.
  • Function-by-template: конкретная функция после инстанцирования function template (условно Max_double из Max<T>).
  • Class-by-template: конкретный класс после инстанцирования class template (например Stack<int> из Stack<T>).
  • Implicit instantiation (неявное инстанцирование): инстанцирование, которое компилятор запускает сам при вызове function template с типизированными аргументами.
  • Explicit instantiation (явное инстанцирование): программист явно задаёт тип в угловых скобках (например spaceOf<int>()), когда тип из аргументов вывести нельзя.
  • Non-type template parameter: параметр шаблона — константное значение compile time (часто целое), а не тип; например template <typename T, int N>.
  • Code bloat (раздувание кода): когда инстанцирование даёт несколько копий одной и той же function-by-template в разных объектных файлах и раздувает исполняемый файл.
  • Type requirements (требования к типу): набор операций, который фактический тип должен поддерживать в шаблоне; иначе — compile-time errors.
  • Concept (C++20) (концепт): формальное, проверяемое компилятором описание требований к параметрам шаблона; яснее диагностика при нарушении.
  • Genericity (обобщённость): возможность писать код для любого типа, удовлетворяющего требованиям, а не только для фиксированных типов.
  • RANGE type: пользовательский тип, ограничивающий целые значения интервалом на compile time; обычно class template с non-type границами.
  • Metaprogramming (метапрограммирование): техника, где программа на compile time манипулирует типами/значениями через шаблоны, а не логикой runtime.
  • Raw pointer (сырой указатель): обычный указатель C/C++ (T*) без встроенной семантики владения и автоочистки.
  • Dangling pointer (висячий указатель): указатель на память уже освобождённую или вышедшую из области видимости; разыменование — undefined behavior.
  • Memory leak (утечка памяти): объект в heap не освобождён, потому что последний указатель исчез без delete; память занята, но недоступна.
  • RAII (Resource Acquisition Is Initialization): идиома C++ — ресурс (память, дескриптор файла, mutex и т.д.) захватывается в конструкторе и освобождается в деструкторе при выходе объекта из области видимости.
  • Smart pointer (умный указатель): class template, оборачивающий raw pointer и добавляющий управление ресурсом (и при необходимости учёт владения) через RAII.
  • unique_ptr: exclusive ownership — в каждый момент владеет объектом не более одного unique_ptr; при уничтожении указателя объект удаляется. Копирование запрещено; владение переносится через std::move.
  • shared_ptr: shared ownership через ARC; объект удаляется, когда уничтожается последний shared_ptr на него.
  • weak_ptr: non-owning наблюдатель за объектом под shared_ptr без роста reference count; разрывает circular references. Доступ — через shared_ptr из lock().
  • Automatic Reference Counting (ARC): стратегия с целочисленным reference count; объект уничтожается при нуле счётчика владельцев.
  • Reference count: счётчик, который ведёт shared_ptr для числа владеющих shared_ptr данным объектом.
  • Circular reference (циклическая ссылка): два и более объекта держат shared_ptr друг на друга; счётчики не падают до нуля — memory leak.
  • std::move: приведение к rvalue reference; перенос владения у unique_ptr (и других move-only типов) без копирования.
  • make_unique<T>(...) (C++14): фабрика, создающая объект и оборачивающая в unique_ptr за один exception-safe шаг.
  • make_shared<T>(...): фабрика, создающая объект и control block со счётчиками вместе, возвращает shared_ptr.
  • weak_ptr::expired(): true, если наблюдаемый объект уже уничтожен (счётчик владельцев обнулился).
  • weak_ptr::lock(): пытается получить shared_ptr на объект; если объекта нет — пустой shared_ptr.
  • Control block (блок управления): внутренняя структура рядом с управляемым объектом в shared_ptr; хранит reference count и счётчик weak ссылок.

3. Примеры

3.1. Обобщённый стек и специализация StringStack (Лаба 5, Задание 1)

Реализуйте class template GenericStack<T> с операциями push(), pop() и peek(). Стек должен динамически менять размер. Затем создайте подкласс StringStack, который:

  • при push() отклоняет пустые строки;
  • добавляет метод concatTopTwo(): снимает две верхние строки, конкатенирует и кладёт результат обратно.

Предусмотрите обработку ошибок на граничных случаях.

Нажмите, чтобы увидеть решение

Ключевая идея: templates и inheritance работают вместе: StringStack наследует GenericStack<string>, получает всю обобщённую функциональность и настраивает/расширяет её для строк.

#include <iostream>
#include <vector>
#include <stdexcept>
#include <string>
using namespace std;

// -------- GenericStack<T> --------
template <typename T>
class GenericStack {
protected:
    vector<T> data;   // vector handles dynamic resizing automatically

public:
    // Constructor: optional initial capacity (vector still grows as needed)
    explicit GenericStack(int initialCapacity = 0) {
        data.reserve(initialCapacity);
    }

    virtual ~GenericStack() = default;

    // Insert element on top
    virtual void push(const T& element) {
        data.push_back(element);
    }

    // Remove and return top element
    virtual T pop() {
        if (isEmpty()) throw underflow_error("pop() on empty stack");
        T top = data.back();
        data.pop_back();
        return top;
    }

    // Return top element without removing it
    virtual T peek() const {
        if (isEmpty()) throw underflow_error("peek() on empty stack");
        return data.back();
    }

    bool isEmpty() const { return data.empty(); }
    int  size()    const { return (int)data.size(); }
};

// -------- StringStack --------
class StringStack : public GenericStack<string> {
public:
    explicit StringStack(int capacity = 0) : GenericStack<string>(capacity) { }

    // Override push: reject empty strings
    void push(const string& s) override {
        if (s.empty())
            throw invalid_argument("StringStack::push: empty string not allowed");
        GenericStack<string>::push(s);   // Delegate to base
    }

    // New method: pop top two strings, concatenate, push result
    void concatTopTwo() {
        if (size() < 2)
            throw underflow_error("concatTopTwo: need at least 2 elements");
        string second = pop();  // top
        string first  = pop();  // second from top
        push(first + second);   // push concatenation
    }
};

int main() {
    cout << "=== GenericStack<int> ===" << endl;
    GenericStack<int> intStack(5);
    intStack.push(10);
    intStack.push(20);
    intStack.push(30);
    cout << "Peek: " << intStack.peek() << endl;  // 30
    cout << "Pop:  " << intStack.pop()  << endl;  // 30
    cout << "Pop:  " << intStack.pop()  << endl;  // 20

    cout << "\n=== StringStack ===" << endl;
    StringStack ss;
    ss.push("Hello, ");
    ss.push("World!");
    cout << "Top before concat: " << ss.peek() << endl;  // "World!"

    ss.concatTopTwo();
    cout << "After concatTopTwo: " << ss.peek() << endl; // "Hello, World!"

    // Try pushing empty string
    try {
        ss.push("");
    } catch (const invalid_argument& e) {
        cout << "Error: " << e.what() << endl;
    }

    // Try concatTopTwo on single element
    try {
        ss.concatTopTwo();
    } catch (const underflow_error& e) {
        cout << "Error: " << e.what() << endl;
    }

    return 0;
}

Вывод:

=== GenericStack<int> ===
Peek: 30
Pop:  30
Pop:  20

=== StringStack ===
Top before concat: World!
After concatTopTwo: Hello, World!
Error: StringStack::push: empty string not allowed
Error: concatTopTwo: need at least 2 elements
  1. База шаблона: GenericStack<T> работает с любым типом; vector<T> сам растёт при необходимости.
  2. Наследование: StringStack : public GenericStack<string> наследует все обобщённые операции.
  3. Переопределение push: сначала проверка, затем делегирование в базу через GenericStack<string>::push(s).
  4. concatTopTwo: снятие в порядке LIFOпервым снимается верх (второе слово), вторым — под ним (первое слово); конкатенация должна восстановить правильный порядок фразы.
  5. Виртуальный деструктор базы: virtual ~GenericStack() = default гарантирует корректное уничтожение через указатель на базу.

Ответ: полная реализация приведена выше. Inheritance вместе с templates даёт аккуратную специализацию обобщённого контейнера.

3.2. Умные указатели на примере класса Box (Лаба 5, Задание 2)

Создайте класс Box с целым полем, конструктором и деструктором (с выводом в консоль). Затем реализуйте и продемонстрируйте:

(a) create_unique(int val) — создаёт Box через unique_ptr, показывает перенос владения и возвращает значение внутри Box.

(b) create_shared_boxes() — создаёт два shared_ptr<Box> и показывает, как меняется reference count.

(c) пример с weak_ptr<Box> — проверка «живости» и получение shared_ptr через lock() для доступа. Объясните, как weak_ptr устраняет circular references.

Нажмите, чтобы увидеть решение

Ключевая идея: у каждого вида smart pointer своя модель владения: unique_ptr — один владелец; shared_ptrshared ownership и ARC; weak_ptrnon-owning наблюдатель.

#include <iostream>
#include <memory>
using namespace std;

// ---- Box class ----
class Box {
public:
    int value;

    Box(int v) : value(v) {
        cout << "Box(" << value << ") created\n";
    }
    ~Box() {
        cout << "Box(" << value << ") destroyed\n";
    }
};

// ---- (a) unique_ptr: exclusive ownership ----
int create_unique(int val) {
    auto box = make_unique<Box>(val);   // Box created, ref count = 1 (conceptually)
    cout << "box value: " << box->value << endl;

    // Transfer ownership: box2 takes over, box becomes null
    auto box2 = move(box);
    cout << "After move: box is " << (box ? "valid" : "null") << endl;
    cout << "box2 value: " << box2->value << endl;

    int result = box2->value;
    return result;
    // box2 goes out of scope → Box destroyed automatically
}

// ---- (b) shared_ptr: cooperative ownership ----
void create_shared_boxes() {
    auto boxA = make_shared<Box>(10);   // ref count = 1
    cout << "boxA ref count: " << boxA.use_count() << endl;  // 1

    auto boxB = make_shared<Box>(20);   // ref count = 1
    cout << "boxB ref count: " << boxB.use_count() << endl;  // 1

    {
        auto boxA2 = boxA;              // ref count = 2 (shared ownership)
        cout << "After boxA2 = boxA, ref count: " << boxA.use_count() << endl;  // 2

        auto boxA3 = boxA;              // ref count = 3
        cout << "After boxA3 = boxA, ref count: " << boxA.use_count() << endl;  // 3
    }
    // boxA2 and boxA3 go out of scope → ref count = 1, object NOT deleted
    cout << "After scope: boxA ref count: " << boxA.use_count() << endl;  // 1
}
// boxA goes out of scope → ref count = 0 → Box(10) destroyed

// ---- (c) weak_ptr: non-owning reference ----
void demonstrate_weak() {
    auto shared = make_shared<Box>(42);  // ref count = 1
    weak_ptr<Box> weak(shared);          // ref count still = 1

    cout << "shared ref count: " << shared.use_count() << endl;  // 1
    cout << "weak expired? "     << weak.expired() << endl;       // 0 (false)

    // Safe access via lock()
    if (auto temp = weak.lock()) {
        cout << "Accessed via weak: " << temp->value << endl;  // 42
    }

    shared.reset();  // Explicitly destroy the shared_ptr → ref count = 0 → Box destroyed
    cout << "After reset, weak expired? " << weak.expired() << endl;  // 1 (true)

    if (!weak.lock()) {
        cout << "Object is gone — weak_ptr is safe to use (returns null)\n";
    }
}

int main() {
    cout << "=== (a) unique_ptr ===" << endl;
    int v = create_unique(100);
    cout << "Returned value: " << v << "\n" << endl;

    cout << "=== (b) shared_ptr ===" << endl;
    create_shared_boxes();
    cout << endl;

    cout << "=== (c) weak_ptr ===" << endl;
    demonstrate_weak();

    return 0;
}

Вывод:

=== (a) unique_ptr ===
Box(100) created
box value: 100
After move: box is null
box2 value: 100
Box(100) destroyed
Returned value: 100

=== (b) shared_ptr ===
Box(10) created
boxA ref count: 1
Box(20) created
boxB ref count: 1
After boxA2 = boxA, ref count: 2
After boxA3 = boxA, ref count: 3
After scope: boxA ref count: 1
Box(10) destroyed
Box(20) destroyed

=== (c) weak_ptr ===
shared ref count: 1
weak expired? 0
Accessed via weak: 42
Box(42) destroyed
After reset, weak expired? 1
Object is gone — weak_ptr is safe to use (returns null)
  1. unique_ptr (a): move(box) переносит владение и обнуляет box. Объект уничтожается, когда box2 выходит из области видимости create_unique.
  2. shared_ptr (b): каждая копия shared_ptr (присваивание или копирующий конструктор) увеличивает use_count(). Когда все копии уничтожены, use_count() становится 0 и объект удаляется.
  3. weak_ptr (c):
    • weak_ptr<Box> weak(shared) — наблюдатель без роста reference count;
    • shared.reset() обнуляет счётчик владельцев → Box удаляется;
    • после уничтожения weak.expired() даёт true;
    • weak.lock() безопасно возвращает пустой shared_ptr — без undefined behavior.
  4. Циклы: если бы в Box были взаимные shared_ptr<Box>, объекты не освободились бы; одну дугу заменяют на weak_ptr<Box>.

Ответ: полный код выше демонстрирует все три smart pointer и семантику времени жизни.

3.3. Шаблон функции Max: максимум из двух значений (Лекция 5, Пример 1)

Напишите function template Max, возвращающий максимум из двух значений любого типа с operator>. Покажите вызовы для int, double и пользовательского класса.

Нажмите, чтобы увидеть решение

Ключевая идея: function template пишется один раз с параметром типа T; компилятор порождает конкретные версии (Max_int, Max_double и т.д.) по типам аргументов в каждой точке вызова.

#include <iostream>
using namespace std;

// Function template: works for any type T that has operator>
template <typename T>
T Max(T a, T b) {
    return a > b ? a : b;
}

// A user-defined class that satisfies the template requirement (has operator>)
class Temperature {
    double celsius;
public:
    Temperature(double c) : celsius(c) { }
    bool operator>(const Temperature& t) const { return celsius > t.celsius; }
    double value() const { return celsius; }
};

int main() {
    // Implicit instantiation for int (compiler generates Max_int)
    cout << Max(3, 7) << endl;           // 7

    // Implicit instantiation for double (compiler generates Max_double)
    cout << Max(3.14, 2.71) << endl;     // 3.14

    // Implicit instantiation for Temperature (compiler generates Max_Temperature)
    Temperature t1(36.6), t2(38.2);
    cout << Max(t1, t2).value() << endl; // 38.2

    return 0;
}
  1. Объявление шаблона: template <typename T> вводит параметр типа T; тело return a > b ? a : b; использует T везде, где нужен тип.
  2. Implicit instantiation: для Max(3, 7) компилятор выводит T = int и внутренне порождает функцию вида int Max_int(int a, int b).
  3. Требования к типу: у Temperature должен быть operator> — иначе ошибка внутри сгенерированной Max_Temperature.
  4. Разные инстансы: Max_int, Max_double и Max_Temperature — три разные функции в скомпилированном коде.

Ответ: из одного определения шаблона получаются три функции. Вывод: 7, 3.14, 38.2.

3.4. alignArray: от конкретной функции к шаблону (Лекция 5, Пример 2)

По следующей функции только для int постройте function template для массивов произвольного «числового» типа элементов. Объявите класс, удовлетворяющий type requirements шаблона, и продемонстрируйте шаблон на массиве этого класса.

void alignArray(int* array, int size, int barrier) {
    for (int i = 0; i < size; i++) {
        if      (array[i] < barrier) array[i] += 2;
        else if (array[i] > barrier) array[i] -= 2;
    }
}
Нажмите, чтобы увидеть решение

Ключевая идея: перечислите операции, которые функция применяет к элементам; это и есть требования к параметру типа T.

  1. Операции над элементами: <, >, +=, -= — тип T должен поддерживать все четыре.
  2. Шаблон:
#include <iostream>
using namespace std;

template <typename T>
void alignArray(T* array, int size, T barrier) {
    for (int i = 0; i < size; i++) {
        if      (array[i] < barrier) array[i] += 2;
        else if (array[i] > barrier) array[i] -= 2;
    }
}
  1. Класс, удовлетворяющий требованиям:
class Score {
    int val;
public:
    Score(int v = 0) : val(v) { }
    bool operator<(const Score& s) const { return val < s.val; }
    bool operator>(const Score& s) const { return val > s.val; }
    Score& operator+=(int n) { val += n; return *this; }
    Score& operator-=(int n) { val -= n; return *this; }
    int value() const { return val; }
};
  1. Демонстрация на классе:
int main() {
    // Demo with int
    int intArr[] = {1, 5, 10, 3, 7};
    alignArray(intArr, 5, 5);
    for (int x : intArr) cout << x << " ";  // 3 5 8 5 5
    cout << endl;

    // Demo with Score
    Score scores[] = {Score(1), Score(8), Score(5), Score(3)};
    alignArray(scores, 4, Score(5));
    for (auto& s : scores) cout << s.value() << " ";  // 3 6 5 5
    cout << endl;

    return 0;
}

Ответ: шаблон задаёт и тип элементов, и тип порога одним параметром T. Любой тип с <, >, +=, -= подходит.

3.5. Шаблон стека с non-type параметром размера (Лекция 5, Пример 3)

Реализуйте обобщённый class template Stack с параметрами: тип элементов T и максимальный размер N. Покажите создание стеков разных типов и размеров.

Нажмите, чтобы увидеть решение

Ключевая идея: non-type template parameters задают «габариты» класса на compile time. Stack<int,10> и Stack<int,50>разные типы.

#include <iostream>
#include <stdexcept>
using namespace std;

template <typename T, int N>
class Stack {
    int top;
    T S[N];          // Array size N is a compile-time constant
public:
    Stack() : top(-1) { }

    void push(const T& V) {
        if (top >= N - 1) throw overflow_error("Stack is full");
        S[++top] = V;
    }

    T pop() {
        if (top < 0) throw underflow_error("Stack is empty");
        return S[top--];
    }

    T peek() const {
        if (top < 0) throw underflow_error("Stack is empty");
        return S[top];
    }

    bool isEmpty() const { return top < 0; }
    bool isFull()  const { return top >= N - 1; }
};

int main() {
    Stack<int, 10> intStack;
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);
    cout << intStack.pop() << endl;  // 3 (LIFO order)
    cout << intStack.pop() << endl;  // 2

    Stack<string, 5> strStack;
    strStack.push("hello");
    strStack.push("world");
    cout << strStack.pop() << endl;  // "world"

    // Stack<int, 10> and Stack<int, 50> are DIFFERENT types:
    // Stack<int, 50> bigStack = intStack;  // COMPILE ERROR

    return 0;
}
  1. Объявление шаблона: template <typename T, int N> — два параметра: тип элементов и вместимость.
  2. Массив: T S[N]N вычисляется на compile time и годится как размер массива.
  3. Семантика LIFO: push увеличивает top перед записью; pop читает на top, затем уменьшает.
  4. Контроль границ: при переполнении/пустом стеке — стандартные исключения.

Ответ: Stack<int,10> и Stack<string,5> — независимые типы из одного шаблона. Вывод: 3, 2, world.

3.6. spaceOf: явное инстанцирование шаблона функции (Лекция 5, Пример 4)

Напишите function template spaceOf<T>(), который считает, сколько 32-битных слов (по 4 байта) нужно, чтобы разместить значение типа T. Покажите explicit instantiation для нескольких типов.

Нажмите, чтобы увидеть решение

Ключевая идея: если у шаблона функции нет аргументов для вывода T, тип нужно указать явно — синтаксисом <T> в месте вызова.

#include <iostream>
using namespace std;

template <typename T>
int spaceOf() {
    int bytes = sizeof(T);
    // Number of 32-bit words: ceiling division by 4
    return bytes / 4 + (bytes % 4 > 0 ? 1 : 0);
}

class MyData {
    double x, y, z;    // 3 doubles = 24 bytes
    int flag;          // 4 bytes
    // Total: 28 bytes = 7 × 4-byte words
};

int main() {
    // Cannot call spaceOf() without explicit type — no arguments to deduce from
    cout << spaceOf<int>()    << endl;    // 4 bytes  → 1 word
    cout << spaceOf<double>() << endl;    // 8 bytes  → 2 words
    cout << spaceOf<char>()   << endl;    // 1 byte   → 1 word (ceiling)
    cout << spaceOf<MyData>() << endl;    // 28+ bytes → 7+ words (depends on padding)

    // The compiler optimizes: sizeof(int)=4 is a compile-time constant,
    // so spaceOf<int>() is likely inlined to the constant 1.
    return 0;
}
  1. Нечем вывести тип: у spaceOf() нет параметров, компилятор не выведет T.
  2. Явный синтаксис: spaceOf<int>() — угловые скобки задают T = int.
  3. Оптимизация на этапе компиляции: sizeof(T)compile-time constant, функция может полностью свернуться в константу.
  4. Формула потолка: bytes/4 + (bytes%4 > 0 ? 1 : 0) даёт \(\lceil \text{bytes}/4 \rceil\).

Ответ: spaceOf<int>() = 1, spaceOf<double>() = 2, spaceOf<char>() = 1.

3.7. Шаблон RANGE: полная реализация (Туториал 5, Пример 1)

Напишите полную реализацию шаблона RANGE, включая:

  • конструктор(ы) и деструктор;
  • арифметические и отношения (+=, -=, +, -, ==, !=, <, >);
  • операторы инкремента и декремента;
  • приведение RANGE → long;
  • проверку границ и исключения при нарушении.

Приведите два реалистичных примера использования RANGE.

Нажмите, чтобы увидеть решение

Ключевая идея: non-type template parameters переносят границы диапазона в тип, а не в значение. Поэтому RANGE<1,12> и RANGE<1,31> — несовместимые типы: компилятор не даст их перепутать.

#include <iostream>
#include <stdexcept>
using namespace std;

template <int L, int R>
class RANGE {
    int value;

    void check() const {
        if (value < L || value > R)
            throw out_of_range("RANGE value " + to_string(value)
                               + " out of [" + to_string(L) + "," + to_string(R) + "]");
    }

public:
    // Constructor from int
    explicit RANGE(int v) : value(v) { check(); }

    // Copy constructor
    RANGE(const RANGE& r) : value(r.value) { }

    // Destructor (trivial)
    ~RANGE() = default;

    // --- Assignment ---
    RANGE& operator=(const RANGE& r) { value = r.value; return *this; }
    RANGE& operator=(int v)          { value = v; check(); return *this; }

    // --- Compound arithmetic ---
    RANGE& operator+=(int n) { value += n; check(); return *this; }
    RANGE& operator-=(int n) { value -= n; check(); return *this; }

    // --- Binary arithmetic (return plain int for flexibility) ---
    int operator+(int n)          const { return value + n; }
    int operator-(int n)          const { return value - n; }
    int operator+(const RANGE& r) const { return value + r.value; }
    int operator-(const RANGE& r) const { return value - r.value; }

    // --- Increment / Decrement ---
    RANGE& operator++()    { value++; check(); return *this; }  // pre-increment
    RANGE  operator++(int) { RANGE tmp(*this); ++(*this); return tmp; } // post-increment
    RANGE& operator--()    { value--; check(); return *this; }  // pre-decrement
    RANGE  operator--(int) { RANGE tmp(*this); --(*this); return tmp; } // post-decrement

    // --- Relational ---
    bool operator==(const RANGE& r) const { return value == r.value; }
    bool operator!=(const RANGE& r) const { return value != r.value; }
    bool operator< (const RANGE& r) const { return value <  r.value; }
    bool operator> (const RANGE& r) const { return value >  r.value; }

    // --- Conversion to long ---
    operator long() const { return (long)value; }
};

// ---- Practical Examples ----

using DayOfMonth = RANGE<1, 31>;
using MonthOfYear = RANGE<1, 12>;

int main() {
    // Example 1: Calendar date arithmetic
    DayOfMonth   day(15);
    MonthOfYear  month(6);

    cout << "Day: "   << (long)day   << endl;  // 15
    cout << "Month: " << (long)month << endl;  // 6

    ++day;
    cout << "Next day: " << (long)day << endl;  // 16

    try {
        day = 32;  // out of range!
    } catch (const out_of_range& e) {
        cout << "Error: " << e.what() << endl;
    }

    // day = month;  // COMPILE ERROR: incompatible types — safety guaranteed!

    // Example 2: Traffic light phase (0=Red, 1=Yellow, 2=Green)
    using Phase = RANGE<0, 2>;
    Phase light(0);
    for (int i = 0; i < 4; i++) {
        cout << "Light phase: " << (long)light << endl;
        try { ++light; } catch (...) { light = Phase(0); }  // wrap around on overflow
    }

    return 0;
}
  1. Параметры шаблона как идентичность типа: DayOfMonth (псевдоним RANGE<1,31>) и MonthOfYear (RANGE<1,12>) — разные типы; смешать их — ошибка компиляции.
  2. check() на каждой мутации: конструктор, operator=, operator+=, operator++ и т.д.
  3. Приведение к long: удобно печатать и подставлять значение в выражения без лишних приведений.
  4. Постфиксный ++: нужна копия «старого» значения до инкремента.

Ответ: полный код выше. Границы фиксируются и на compile time (через тип), и на runtime (через исключения).

3.8. Шаблон ARRAY с границами индекса как у RANGE (Туториал 5, Пример 2)

Спроектируйте и реализуйте шаблон ARRAY, где допустимый диапазон индексов задаётся параметрами шаблона (по духу как у RANGE). Массив должен поддерживать произвольные границы индекса (не обязательно с нуля). Реализуйте operator[] с проверкой границ. Покажите практический пример.

Нажмите, чтобы увидеть решение

Ключевая идея: как RANGE вносит границы значения в тип, так ARRAY может внести границы индекса. ARRAY<int,1,12> — 12 целых с индексами 1…12, т.е. «массив с базой 1».

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;

// ARRAY<T, Low, High>: an array of type T with indices in [Low, High]
template <typename T, int Low, int High>
class ARRAY {
    static_assert(Low <= High, "ARRAY: Low must be <= High");
    static const int SIZE = High - Low + 1;
    T data[SIZE];

    void checkIndex(int idx) const {
        if (idx < Low || idx > High)
            throw out_of_range("ARRAY index " + to_string(idx)
                               + " out of [" + to_string(Low) + "," + to_string(High) + "]");
    }

public:
    ARRAY() = default;

    // Non-const indexing (allows modification)
    T& operator[](int idx) {
        checkIndex(idx);
        return data[idx - Low];   // Shift: external index → internal 0-based index
    }

    // Const indexing (for read-only access)
    const T& operator[](int idx) const {
        checkIndex(idx);
        return data[idx - Low];
    }

    int low()  const { return Low;  }
    int high() const { return High; }
    int size() const { return SIZE; }
};

int main() {
    // 1-based array of month names (indices 1..12)
    ARRAY<string, 1, 12> monthNames;
    monthNames[1]  = "January";
    monthNames[2]  = "February";
    monthNames[3]  = "March";
    // ... (fill remaining)
    monthNames[12] = "December";

    cout << monthNames[1]  << endl;  // "January"
    cout << monthNames[12] << endl;  // "December"

    try {
        monthNames[0];   // Index 0 is out of range [1, 12]
    } catch (const out_of_range& e) {
        cout << "Error: " << e.what() << endl;
    }

    // 2D array using ARRAY of ARRAYs:
    ARRAY<ARRAY<int, 1, 3>, 1, 3> matrix;
    for (int i = 1; i <= 3; i++)
        for (int j = 1; j <= 3; j++)
            matrix[i][j] = i * 10 + j;

    cout << matrix[2][3] << endl;  // 23

    return 0;
}
  1. Сдвиг индекса: внутри хранение 0-основано (data[0..SIZE-1]); снаружи индексы [Low..High]; формула data[idx - Low].
  2. static_assert: проверка аргументов шаблона на compile time (например ARRAY<int, 10, 5> не скомпилируется с ясным сообщением).
  3. Два operator[]: для изменяемого доступа (T&) и для const массива (const T&).
  4. 2D: вложенный ARRAY в ARRAY даёт матрицу с проверкой обоих индексов.

Ответ: ключевой приём — сдвиг data[idx - Low]. Любой диапазон индексов задаётся типом; выход за границы ловится в runtime.

3.9. Обобщённый умный указатель: реализация RAII (Туториал 5, Пример 3)

Реализуйте простой class template smart_pointer<T>, который:

  • принимает владение raw pointer, переданным в конструктор;
  • автоматически удаляет объект при выходе умного указателя из области видимости;
  • поддерживает -> и * в стиле обычного указателя;
  • добавьте операторы, максимально приближающие поведение к raw pointer;
  • напишите тест, показывающий преимущество перед «сырыми» указателями.
Нажмите, чтобы увидеть решение

Ключевая идея: RAII гарантирует вызов деструктора при выходе из области видимости — в том числе при раскрутке стека по исключению. На этом строятся все smart pointers.

#include <iostream>
#include <stdexcept>
using namespace std;

template <typename T>
class smart_pointer {
    T* obj;

public:
    // Constructor: acquire ownership
    explicit smart_pointer(T* o = nullptr) : obj(o) { }

    // Destructor: release ownership (RAII core)
    ~smart_pointer() {
        delete obj;
        cout << "[smart_pointer: object deleted]" << endl;
    }

    // Prevent copying (like unique_ptr)
    smart_pointer(const smart_pointer&) = delete;
    smart_pointer& operator=(const smart_pointer&) = delete;

    // Pointer-like operators
    T* operator->() { return obj; }
    T& operator*()  { return *obj; }

    // Conversion to raw pointer (read-only)
    T* get() const { return obj; }

    // Boolean check: is the pointer non-null?
    explicit operator bool() const { return obj != nullptr; }

    // Comparison with nullptr
    bool operator==(nullptr_t) const { return obj == nullptr; }
    bool operator!=(nullptr_t) const { return obj != nullptr; }
};

// A sample class to manage
class Resource {
    int id;
public:
    Resource(int i) : id(i) { cout << "Resource " << id << " created\n"; }
    ~Resource()              { cout << "Resource " << id << " destroyed\n"; }
    void use()               { cout << "Using Resource " << id << "\n"; }
};

void demonstrateAdvantage() {
    // With raw pointers: must remember to delete!
    // Resource* raw = new Resource(99);
    // raw->use();
    // if (someCondition) return;  // MEMORY LEAK if we forget delete here
    // delete raw;

    // With smart_pointer: automatic cleanup, even on early return
    smart_pointer<Resource> sp(new Resource(1));
    sp->use();      // Works like a raw pointer
    (*sp).use();    // Also works

    if (sp) {
        cout << "Pointer is valid\n";
    }

    cout << "Leaving scope...\n";
}   // smart_pointer destructor called automatically here — no delete needed!

int main() {
    demonstrateAdvantage();
    cout << "After scope exit\n";
    return 0;
}

Вывод:

Resource 1 created
Using Resource 1
Using Resource 1
Pointer is valid
Leaving scope...
[smart_pointer: object deleted]
Resource 1 destroyed
After scope exit
  1. RAII: ~smart_pointer() вызывает delete obj — при выходе из области или при исключении.
  2. Запрет копирования: иначе два smart_pointer могли бы владеть одним объектом (double-delete).
  3. operator->: возвращает сырой указатель для синтаксиса sp->use().
  4. operator bool(): проверки вида if (sp).
  5. Плюс над raw: даже при раннем выходе или исключении деструктор сработает — нет memory leak.

Ответ: критичен деструктор по RAII; последовательность вывода выше показывает автоматическую очистку.